package emulator.analyzer.sid;

import java.io.PrintStream;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import emulator.analyzer.Machine;
import emulator.analyzer.util.RangeRelocation;
import emulator.analyzer.util.StatConverter;
import emulator.hardware.HwWord;
import emulator.util.AddressRange;
import emulator.util.AddressRangeList;
import emulator.util.file.FileHelper;
import emulator.util.file.FileInfo;
import emulator.util.file.FileLoadStrategy;

public class SidProcessor {
	static Logger logger = LogManager.getLogger(SidProcessor.class.getName());
	private static final int max_zp_bytes = 32;
	private Machine machine = new Machine();
	private String sid_name;
	private SidInfo sid_info = null;
	private int max_play_time = 300*60; /* 5 min. */
	private int zp_count;
	private boolean is_stereo;
	private int code_count;
	private AddressRangeList unknown_list;
	private byte [] sid_data;
	private AddressRangeList zp_list = null;
	private PrintStream out;
	final static AddressRange ADDR_RANGE_ZP = new AddressRange(0, 0xff);
	final static AddressRange ADDR_RANGE_STACK = new AddressRange(0x0100, 0x01ff);
	final static AddressRange ADDR_RANGE_SID1  = new AddressRange(0xD400, 0xD41F);
	final static AddressRange ADDR_RANGE_SID2  = new AddressRange(0xD440, 0xD45F);
	
	public SidProcessor(String sid_name) {
		this.sid_name = sid_name;
		machine.start();
	}
	
	public SidProcessor(String sid_file_name, PrintStream out) {
		this.sid_name = sid_file_name;
		this.out = out;
		machine.start();
	}

	public boolean processSid(int first_song, int last_song, boolean verbose)
	{
		sid_info = loadSid();
		if (isValid())
			return analzeSid(first_song, last_song, verbose);
		return false;
	}

	public boolean isValid() {
		return sid_info != null;
	}

	public void printResult(PrintStream out) {
		out.println("Execution: " + machine.getCycles() + " cycles / " + machine.getTime() + " (PAL)");
		out.println("------------------------------------------------------------");
		out.println("File: "+sid_name);
		sid_info.print(out);
		out.println("------------------------------------------------------------");
		machine.dumpMemoryStats(out);
		out.println("------------------------------------------------------------");
		out.println("ZP usage: " + zp_count + " bytes");
		int code_size = sid_info.getBlockSize();
		if (machine.getDynArea() != null)
		{
			out.println("Dynamic code area: " + machine.getDynArea());
			code_size += machine.getDynArea().getWidth();
		}
		out.println("Code coverage: " + code_count + "/" + code_size + " (" + code_count*100/code_size + "%)");
		out.println("Stereo SID detected: " + (is_stereo ? "yes" : "no"));
		out.println("Unidentified memory access:");
		unknown_list.dump(out, " ");
		out.println("------------------------------------------------------------");
		out.println("SID Analysis complete.");
	}

	public SidInfo loadSid()
	{
		FileInfo info = FileHelper.getFileInfo(sid_name, FileHelper.guessFileFormat(sid_name));
		if (info == null)
		{
			logger.error("Unknown file format or file \""+sid_name+"\" not found.");
			return null;
		}
		if (info.getStartAddress() < 0)
		{
			logger.error("Unknown load address for file \""+sid_name+"\"");
			return null;
		}
		FileLoadStrategy loader = info.getLoadStrategy();
		if (loader == null)
		{
			logger.error("Unsupported file format for file \""+sid_name+"\"");
			return null;
		}
		loader.loadToMemory(sid_name, machine.getMemory(), info.getStartAddress(), info.getBlockSize());
		sid_data = Arrays.copyOfRange(machine.getMemory().getData(), info.getStartAddress(), info.getStartAddress() + info.getBlockSize());
		return new SidInfo(info);
	}
	
	private boolean analzeSid(int first_song, int last_song, boolean verbose) {
		boolean result = false;
		machine.init(sid_info.getCodeRange(), out);
		for (int song = first_song; song <= last_song && song <= sid_info.getSongCount(); song++)
		{
			machine.getMemory().getData()[0x02A6] = 1; /* PAL timing */
			if (verbose) out.println("Analyzing song "+song+"/"+sid_info.getSongCount()+" ...");
			if (!machine.analyzeSubroutine(sid_info.getInitAddress(), song, 0, 0))
			{
				out.println("-> INIT failed!");
				continue;
			}
			if (!sid_info.getCodeRange().contains(sid_info.getExecAddress()))
			{
				if (machine.getDynArea() != null)
				{
					if (!machine.getDynArea().contains(sid_info.getExecAddress()))
					{
						out.println("Invalid EXEC address "+new HwWord(sid_info.getExecAddress()));
						continue;
					}
				}
				else if (!machine.activateArea(sid_info.getExecAddress()))
				{
					out.println("Can not activate EXEC address "+new HwWord(sid_info.getExecAddress()));
					continue;
				}
			}
			for (int cycle = 0; cycle < max_play_time; cycle++)
			{
				if (!machine.analyzeSubroutine(sid_info.getExecAddress(), 0, 0, 0))
				{
					out.println("-> EXEC failed for cycle " + cycle + "!");
					break;
				}
			}
			result = true;
		}
		boolean [] access_field = machine.getAccessField();
		int memory_size = access_field.length;
		boolean [] zp_mask = StatConverter.rangeToMask(SidProcessor.ADDR_RANGE_ZP, memory_size);
		boolean [] stack_mask = StatConverter.rangeToMask(SidProcessor.ADDR_RANGE_STACK, memory_size);
		boolean [] sid1_mask = StatConverter.rangeToMask(SidProcessor.ADDR_RANGE_SID1, memory_size);
		boolean [] sid2_mask = StatConverter.rangeToMask(SidProcessor.ADDR_RANGE_SID2, memory_size);
		boolean [] code_mask = StatConverter.rangeToMask(sid_info.getCodeRange(), memory_size);
		AddressRange dyn_range = machine.getDynArea();
		if (dyn_range != null)
			code_mask = StatConverter.or(code_mask, StatConverter.rangeToMask(dyn_range, memory_size));

		zp_count = StatConverter.count(StatConverter.and(access_field, zp_mask));
		is_stereo = StatConverter.count(StatConverter.and(access_field, sid2_mask)) > 0;
		code_count = StatConverter.count(StatConverter.and(access_field, code_mask));
		
		if (zp_count > 0)
			zp_list = new AddressRangeList(StatConverter.and(access_field, zp_mask));
		
		boolean [] known_mask = StatConverter.or(StatConverter.or(StatConverter.or(zp_mask, stack_mask), StatConverter.or(sid1_mask, sid2_mask)), code_mask);
		unknown_list = new AddressRangeList(StatConverter.and(access_field, StatConverter.not(known_mask)));
		
		return result;
	}

	public void setOut(PrintStream out) {
		this.out = out;
	}

	public SidImage getSidAt(int new_base_address, int new_zp_base, int new_sid_base, int new_hwid)
	{
		new_base_address = alignAddress(new_base_address, sid_info.getStartAddress());
		SidImage sid_image = new SidImage(sid_info);
		
		List<RangeRelocation> relocations = new LinkedList<RangeRelocation>();
		if (zp_list != null)
			zp_list.toRelocations(relocations, new_zp_base);
		relocations.add(new RangeRelocation(SidProcessor.ADDR_RANGE_SID1, new_sid_base - SidProcessor.ADDR_RANGE_SID1.getStart()));
		relocations.add(new RangeRelocation(new AddressRange(0x02A6, 0x02A6), new_hwid - 0x02A6));
		
		RangeRelocation dyn_relocation = null;
		if (machine.getDynArea() != null)
		{
			int new_dyn_start = alignAddress(new_base_address + sid_info.getBlockSize(), machine.getDynArea().getStart());
			dyn_relocation = new RangeRelocation(machine.getDynArea(), new_dyn_start - machine.getDynArea().getStart());
		}
		sid_image.relink(machine.getAddressMap(), sid_data,
					new RangeRelocation(sid_info.getCodeRange(), new_base_address - sid_info.getStartAddress()),
					dyn_relocation,
					machine.getDynFlow(),
					relocations,
					machine.getCmpCommands());
		
		return sid_image;
	}

	private int alignAddress(int address, int align_target) {
		if ((address & 0xff) != (align_target & 0xff))
			address += (align_target - address) & 0xff;
		return address;
	}

	public boolean isConvertiblePrinting(PrintStream out)
	{
		if (zp_count > max_zp_bytes)
		{
			out.println("Too many ZP allocations ("+zp_count+"/"+max_zp_bytes+").");
			return false;
		}
		if (is_stereo)
		{
			out.println("Stereo SID not supported.");
			return false;
		}
		if (!unknown_list.isEmpty())
		{
			out.println("Unidentified memory access:");
			unknown_list.dump(out, " ");
			return false;
		}
		return true;
	}
}
